/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package npuzzle;

import algoritmo.AStar;
import algoritmo.AbstractSearch;
import algoritmo.BidirectionalSearch;
import algoritmo.BreadthFirstSearch;
import algoritmo.DepthFirstSearch;
import algoritmo.DepthLimitedSearch;
import algoritmo.Heuristic;
import algoritmo.IterativeDeepeningAStar;
import algoritmo.IterativeDeepeningDFS;
import algoritmo.Node;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.lang.reflect.InvocationTargetException;
import java.util.Stack;
import javax.swing.*;

/**
 *
 * @author JULIAN
 */
public class PuzzleGUI extends JFrame implements ActionListener {

    private static final int DEF_MAX_ITE = 1000;
    private static final int DEF_N = 3;
    private static final int DEF_MOV = 150;
    private JMenuBar jmb;
    private JMenuItem jmiDFS, jmiDL, jmiBFS, jmiID, jmiAs, jmiIDAs, jmiBi, jmiRepetir, jmiCancelar;
    private JMenuItem jmiRandomStart, jmiRandomGoal, jmiRandomStartFromGoal,
            jmiRandomGoalFromStart, jmiDefault;
    private JMenu jmResolver, jmRandom;
    private JPanel jpCentro; //panel del centro, aquí van los dos PuzzleViews
    private JPanel jpPv1, jpPv2; //contenedores para cada PuzzleView
    private PuzzleNode solucion;
    private PuzzleView pvStart, pvGoal;
    private JLabel jlTime;
    private SwingWorker swork;
    private Heuristic heur = new Heuristic() {

        @Override
        public int h(Node n) {
            PuzzleNode pn = (PuzzleNode) n;
            return pn.manhattan(pvGoal.getNode());
        }
    };
    public static Image cuadroRojo = new ImageIcon("cuadroRojo.png").getImage();
    /*
     * Tiene los movimientos que llevan a la solucion, en terminos de las 
     * constantes de direccion de PuzzleNode
     */
    private static Stack<PuzzleNode> pilaSolucion = new Stack<>();
    /*
     * Tamaño del puzzle en casillas por lado
     */
    private int n;

    public PuzzleGUI() {
        super("N-puzzle");
        setResizable(false);
        setDefaultCloseOperation(EXIT_ON_CLOSE);
        this.n = pedirNumero("Número de casillas por lado", DEF_N);

        //Menus
        jmb = new JMenuBar();

        jmiDFS = new JMenuItem("Depth First Search");
        jmiDFS.setActionCommand("Solve DFS");
        jmiDFS.addActionListener(this);

        jmiDL = new JMenuItem("Depth Limited Search");
        jmiDL.setActionCommand("Solve DL");
        jmiDL.addActionListener(this);

        jmiBFS = new JMenuItem("Breadth First Search");
        jmiBFS.setActionCommand("Solve BFS");
        jmiBFS.addActionListener(this);

        jmiID = new JMenuItem("Iterative Deepening DFS");
        jmiID.setActionCommand("Solve ID");
        jmiID.addActionListener(this);

        jmiAs = new JMenuItem("A*");
        jmiAs.setActionCommand("Solve Astar");
        jmiAs.addActionListener(this);

        jmiIDAs = new JMenuItem("IDA*");
        jmiIDAs.setActionCommand("Solve IDAstar");
        jmiIDAs.addActionListener(this);

        jmiBi = new JMenuItem("DFS Bidireccional");
        jmiBi.setActionCommand("Solve Bi");
        jmiBi.addActionListener(this);

        jmiRepetir = new JMenuItem("Repetir animación");
        jmiRepetir.setActionCommand("Repeat");
        jmiRepetir.setEnabled(false);
        jmiRepetir.addActionListener(this);
        
        jmiCancelar = new JMenuItem("Cancelar búsqueda");
        jmiCancelar.setActionCommand("Cancel");
        jmiCancelar.setEnabled(false);
        jmiCancelar.addActionListener(this);

        jmiRandomStart = new JMenuItem("Inicio aleatorio");
        jmiRandomStart.setActionCommand("Scramble start");
        jmiRandomStart.addActionListener(this);

        jmiRandomGoal = new JMenuItem("Meta aleatoria");
        jmiRandomGoal.setActionCommand("Scramble goal");
        jmiRandomGoal.addActionListener(this);

        jmiDefault = new JMenuItem("Default");
        jmiDefault.setActionCommand("Default");
        jmiDefault.addActionListener(this);

        jmiRandomGoalFromStart = new JMenuItem("Revolver meta a partir de inicial");
        jmiRandomGoalFromStart.setActionCommand("Scramble from start");
        jmiRandomGoalFromStart.addActionListener(this);

        jmiRandomStartFromGoal = new JMenuItem("Revolver inicio a partir de meta");
        jmiRandomStartFromGoal.setActionCommand("Scramble from goal");
        jmiRandomStartFromGoal.addActionListener(this);

        jmResolver = new JMenu("Resolver");
        jmResolver.add(jmiBFS);
        jmResolver.add(jmiDFS);
        jmResolver.add(jmiDL);
        jmResolver.add(jmiID);
        jmResolver.add(jmiBi);
        jmResolver.add(jmiAs);
        jmResolver.add(jmiIDAs);    
        jmResolver.addSeparator();
        jmResolver.add(jmiRepetir);
        jmResolver.add(jmiCancelar);

        jmRandom = new JMenu("Configurar");

        jmRandom.add(jmiRandomStart);
        jmRandom.add(jmiRandomGoal);
        jmRandom.addSeparator();
        jmRandom.add(jmiRandomStartFromGoal);
        jmRandom.add(jmiRandomGoalFromStart);
        jmRandom.addSeparator();
        jmRandom.add(jmiDefault);

        jmb.add(jmResolver);
        jmb.add(jmRandom);

        jlTime = new JLabel("Escoge un algoritmo");

        //PuzzleViews
        jpCentro = new JPanel();

        pvStart = new PuzzleView(n, PuzzleNode.createReversed(n), null);
        pvGoal = new PuzzleView(n, PuzzleNode.createOrdered(n), cuadroRojo);
        jpPv1 = new JPanel();
        jpPv1.setBorder(BorderFactory.createTitledBorder("Inicial"));
        jpPv1.add(pvStart);

        jpPv2 = new JPanel();
        jpPv2.setBorder(BorderFactory.createTitledBorder("Meta"));
        jpPv2.add(pvGoal);

        jpCentro.setLayout(new GridLayout(1, 2));
        jpCentro.add(jpPv1);
        jpCentro.add(jpPv2);

        //BorderLayout
        setLayout(new BorderLayout());
        add(jmb, BorderLayout.NORTH);
        add(jpCentro, BorderLayout.CENTER);
        add(jlTime, BorderLayout.SOUTH);
        pack();

    }

    /* Inicia la búsqueda de la solución usando el algoritmo especificado.
     * Al encontrar la solución, la muestra animando el puzzle.
     * Si no encuentra solución manda un mensaje.
     */
    public void solve(final AbstractSearch s) {
        s.setStart(pvStart.getNode());
        s.setGoal(pvGoal.getNode());

        jlTime.setText("Buscando...");
        swork = new SwingWorker() {

            long time;

            @Override
            protected Object doInBackground() throws Exception {
                time = System.currentTimeMillis();
                return s.search();
            }

            @Override
            protected void done() {
                try {
                    solucion = (PuzzleNode) get();
                } catch (Exception ignore) {
                }
                long duration = System.currentTimeMillis() - time;
                jlTime.setText("\nTiempo: " + String.format("%.6f", duration / 1000.0) + "[s]");
                if (solucion == null) {
                    JOptionPane.showMessageDialog(null, "No se econtró solución");
                } else {
                    jmiRepetir.setEnabled(true);
                    animarSolucion();
                }
            }
        };swork.execute();
        jmiCancelar.setEnabled(true);

    }

    public PuzzleNode getSolucion() {
        return solucion;
    }

    public void setSolucion(PuzzleNode solucion) {
        this.solucion = solucion;
    }

    /*
     * Permite visualizar la solución que encontró alguno de los algoritmos,
     * moviendo los cuadritos uno por uno en un cierto intervalo de tiempo.
     */
    public void animarSolucion() {

        if (solucion == null) {
            return;
        }

        new Thread(new Runnable() {

            @Override
            public void run() {
                jmRandom.setEnabled(false);
                jmResolver.setEnabled(false);
                
                PuzzleNode ap = solucion;
                pilaSolucion.push(ap);
                while (ap.hasPrev()) {
                    ap = (PuzzleNode) ap.prev();
                    pilaSolucion.push(ap);

                }
                while (!pilaSolucion.isEmpty()) {
                    pvStart.setNode(pilaSolucion.pop());
                    repaint();
                    try {
                        Thread.sleep(200);
                    } catch (InterruptedException ex) {
                    }

                }
                jmRandom.setEnabled(true);
                jmResolver.setEnabled(true);
            }
        }).start();

    }

    public static void main(String[] args) throws InterruptedException,
            InvocationTargetException {
        SwingUtilities.invokeLater(new Runnable() {

            @Override
            public void run() {
                new PuzzleGUI().setVisible(true);;
            }
        });
    }

    @Override
    public void actionPerformed(ActionEvent e) {
        switch (e.getActionCommand()) {
            case "Solve DFS":
                solve(new DepthFirstSearch());
                break;
            case "Solve DL":
                solve(new DepthLimitedSearch(pedirNumero("Profundidad de corte", DEF_MAX_ITE)));
                break;
            case "Solve BFS":
                solve(new BreadthFirstSearch());
                break;
            case "Solve ID":
                solve(new IterativeDeepeningDFS(pedirNumero("Máximo número de iteraciones", DEF_MAX_ITE)));
                break;
            case "Solve Astar":
                solve(new AStar(heur));
                break;
            case "Solve IDAstar":
                solve(new IterativeDeepeningAStar(heur, pedirNumero("Máximo número de iteraciones", DEF_MAX_ITE)));
                break;
            case "Solve Bi":
                solve(new BidirectionalSearch(pedirNumero("Máximo número de iteraciones", DEF_MAX_ITE)));
                break;
            case "Repeat":
                animarSolucion();
                break;
            case "Cancel":
                swork.cancel(true);
                jmiCancelar.setEnabled(false);
                break;
            case "Scramble start":
                pvStart.setNode(PuzzleNode.createRandom(n));
                pvStart.repaint();
                break;
            case "Scramble goal":
                pvGoal.setNode(PuzzleNode.createRandom(n));
                pvGoal.repaint();
                break;
            case "Scramble from goal":
                pvStart.setNode(pvGoal.getNode().scramble(pedirNumero("Número de movimientos", DEF_MOV)));
                pvStart.repaint();
                break;
            case "Scramble from start":
                pvGoal.setNode(pvStart.getNode().scramble(pedirNumero("Número de movimientos", DEF_MOV)));
                pvGoal.repaint();
                break;
            case "Default":
                pvStart.setNode(PuzzleNode.createReversed(n));
                pvGoal.setNode(PuzzleNode.createOrdered(n));
                pvStart.repaint();
                pvGoal.repaint();
                break;
        }

    }

    public int pedirNumero(String mensaje, int defaultVal) {
        mensaje += " (" + defaultVal + ")";
        int i = defaultVal;
        try {
            i = Integer.parseInt(JOptionPane.showInputDialog(null,mensaje,"Entrada",JOptionPane.QUESTION_MESSAGE));
        } catch (NumberFormatException ex) {
        }
        return i;
    }
}
